Skip to content

feat: introduce ClickableRegion component to improve keyboard accessibility for interaction editors - #6094

Merged
AlexVelezLl merged 1 commit into
learningequality:unstablefrom
Abhishek-Punhani:click-region
Aug 21, 2026
Merged

feat: introduce ClickableRegion component to improve keyboard accessibility for interaction editors#6094
AlexVelezLl merged 1 commit into
learningequality:unstablefrom
Abhishek-Punhani:click-region

Conversation

@Abhishek-Punhani

@Abhishek-Punhani Abhishek-Punhani commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

Adding ClickableRegion component to improve keyboard accessibility for interaction editors

References

Closes #6043

Reviewer guidance

Navigate to the Qti-demo-page and and test navigating Choice Editor prompt/choice cards using keyboard (Tab, Enter/Space) to verify accessible aria-label announcements, focus outlines, and seamless interaction with nested controls (TipTap, selection inputs, action buttons) without triggering parent clicks.

AI usage

Used Antigravity for final review and nitpicks.

@Abhishek-Punhani

Copy link
Copy Markdown
Member Author

@AlexVelezLl, Should we add this to other interactions as well?

@AlexVelezLl

Copy link
Copy Markdown
Member

Hi @Abhishek-Punhani! Yes, let's use this for the textEntry prompt, and I just merged the ordering interaction PR; we can refactor that editor as well!

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Abhishek-Punhani! It's really nice these button are finally accessible! I've found couple of things we might be able to simplify on the code.

class="overlay-button"
:aria-label="ariaLabel"
@keydown.enter.prevent="onClick"
@keydown.space.prevent="onClick"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add @click, we don't need to specify these two, as for button elements, the space and enter keystrokes also fire the onClick handler on a button element.

:minHeight="'80px'"
:autofocus="mode === 'edit' && isQuestionOpen"
:imageProcessor="EditorImageProcessor"
:tabindex="isQuestionOpen ? 0 : -1"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need these tab indexes; if it's open, tabindex should also be -1 because we can already navigate through the editor to the input. (i.e., it should always be -1, as we are already handling the tab index in the outer container).

Comment on lines 291 to 303
function handlePromptClick(event) {
if (props.mode !== 'edit') return;
if (event.target.closest('button') || event.target.closest('input')) return;
const closestBtn =
event.target && event.target.closest ? event.target.closest('button') : null;
if (closestBtn && !closestBtn.classList.contains('overlay-button')) return;
const closestInput =
event.target && event.target.closest ? event.target.closest('input') : null;
if (closestInput) return;
if (!isQuestionOpen.value) {
event.stopPropagation();
if (event && event.stopPropagation) event.stopPropagation();
openQuestion();
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need all of this? It's even less needed now because when the editor is already open, the Clickable region cannot fire any click event anymore, right?

return {
borderColor: questionHasError.value ? tokens.error : tokens.fineLine,
cursor: props.mode === 'edit' ? 'pointer' : undefined,
'--clickable-region-hover-bg': palette.blue.v_100,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't think we are using this anywhere.

Comment on lines +247 to +268
const closestBtn =
event.target && event.target.closest ? event.target.closest('button') : null;
if (closestBtn && !closestBtn.classList.contains('overlay-button')) return;
const closestInput =
event.target && event.target.closest ? event.target.closest('input') : null;
if (closestInput) return;
if (!isPromptOpen.value) {
event.stopPropagation();
if (event && event.stopPropagation) event.stopPropagation();
openPrompt();
}
}

function handleItemClick(event, itemId) {
if (props.mode !== 'edit') return;
if (openItemId.value === itemId) return;
if (event.target.closest('button') || event.target.closest('input')) return;
event.stopPropagation();
const closestBtn =
event.target && event.target.closest ? event.target.closest('button') : null;
if (closestBtn && !closestBtn.classList.contains('overlay-button')) return;
const closestInput =
event.target && event.target.closest ? event.target.closest('input') : null;
if (closestInput) return;
if (event && event.stopPropagation) event.stopPropagation();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idem

Comment on lines +251 to +258
function handlePromptClick(event) {
if (props.mode !== 'edit') return;
if (event.target.closest('button') || event.target.closest('input')) return;
const closestBtn =
event.target && event.target.closest ? event.target.closest('button') : null;
if (closestBtn && !closestBtn.classList.contains('overlay-button')) return;
const closestInput =
event.target && event.target.closest ? event.target.closest('input') : null;
if (closestInput) return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idem

@Abhishek-Punhani

Copy link
Copy Markdown
Member Author

@AlexVelezLl , Thanks for pointing this out. I’ve removed the redundant @keydown handlers since @click.stop allows the button to handle Enter and Space activation natively.

I also tested moving @click="onClick" exclusively to the button, but that breaks mouse interactions because .content-wrapper sits above the button (z-index: 1). Removing the z-index would instead cause the button to capture clicks meant for nested interactive elements like radio buttons and checkboxes.

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple of things before merging!


<div
class="clickable-area"
@click="onClick"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just saw that this is similar to how we handle our KCard in KDS 😅, so yeah, that's fine. Could you add a comment here, please? We will introduce an a11y linter soon, and it will flag this as an incorrect pattern, but we should mute the linter instead, so it's best to add a clear comment explaining why a plain div has an @click handler without any tabindex/focus/space/enter management.

Comment on lines +31 to +32
event.target.closest(
'button:not(.overlay-button), input, a, select, textarea, [role="button"]',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I meant to ask about this earlier 😅. Why is this needed? Right now, all elements that sit on top of this region have a .stop modifier in their implementation, so we shouldn't need something like this. We can add a comment specifying that interactive elements on top of it should implement a .stop modifier. This would be the same pattern we use for KCard.

clickable
? instance.proxy.$computedClass({
':hover': { backgroundColor: tokens.fineLine },
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…bility for interaction editors

Signed-off-by: Abhishek-Punhani <punhani.manavabhi@gmail.com>

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @Abhishek-Punhani! LGTM!

@AlexVelezLl
AlexVelezLl merged commit 52835d3 into learningequality:unstable Aug 21, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QTI] Choice editor clickable regions are mouse-only and lack accessible semantics

2 participants